import {
    Color,
    Matrix4,
    Mesh,
    PerspectiveCamera,
    Plane,
    ShaderMaterial,
    Vector3,
    Vector4,
    WebGLRenderTarget,
    HalfFloatType,
    BufferGeometry,
    Texture,
    ShaderChunk
} from 'three';

//@ts-ignore
ShaderChunk.gaussian_blur_def = /*glsl*/ `
    #define GAUSSIAN_BLUR_SIZE 10
    #define GAUSSIAN_SIGMA 2.0

    float GAUSS_KERNEL[GAUSSIAN_BLUR_SIZE];
    float gauss(float x,float sigma){
        return 1.0/(sigma*sqrt(2.0*3.1415926))*exp(-(x*x)/(2.0*sigma*sigma));
    }

    void generateGaussKernel(){
        for(int i = 0;i<GAUSSIAN_BLUR_SIZE;i++){
            float x = float(i) - float(GAUSSIAN_BLUR_SIZE)/2.0;
            GAUSS_KERNEL[i] = gauss(x,GAUSSIAN_SIGMA);
        }
    }
`;
//@ts-ignore
ShaderChunk.gaussian_blur_exe = /*glsl*/ `
    generateGaussKernel();
    vec2 texSize = vec2(0.025);

    for(int i = 0;i<GAUSSIAN_BLUR_SIZE;i++){
        for(int j = 0;j<GAUSSIAN_BLUR_SIZE;j++){
            vec2 offset = vec2(float(i)-float(GAUSSIAN_BLUR_SIZE)/2.0,float(j)-float(GAUSSIAN_BLUR_SIZE)/2.0)*texSize;

            vec4 _uv = vec4(0.0);
            _uv.xy = vUv.xy+offset;
            _uv.zw = vUv.zw;
            reflectorColor += GAUSS_KERNEL[i] * GAUSS_KERNEL[j] * texture2DProj(tDiffuse,_uv);
        }
    }
`;

export interface ReflectorOptions {
    color?: Color | string | number;
    textureWidth?: number;
    textureHeight?: number;
    clipBias?: number;
    multisample?: number;
    map?: Texture;
    wrapS?: number;
    wrapT?: number;
    gaussian?: boolean;
    smooth?: number;
}


export class Reflector extends Mesh {

    readonly type = "Reflector";

    readonly isReflector = true;

    public renderTarget: WebGLRenderTarget;

    private camera: PerspectiveCamera;

    constructor(geometry: BufferGeometry, options: ReflectorOptions = {}) {

        super(geometry);

        this.isReflector = true;

        this.type = 'Reflector';
        this.camera = new PerspectiveCamera();

        const scope = this;

        const color = (options.color !== undefined) ? new Color(options.color) : new Color(0x7F7F7F);
        const textureWidth = options.textureWidth || 512;
        const textureHeight = options.textureHeight || 512;
        const clipBias = options.clipBias || 0;
        const multisample = (options.multisample !== undefined) ? options.multisample : 4;
        const wrapS = (options.wrapS !== undefined) ? options.wrapS : 1;
        const wrapT = (options.wrapT !== undefined) ? options.wrapT : 1;
        const map = (options.map !== undefined) ? options.map : null;
        const gaussian = (options.gaussian !== undefined) ? options.gaussian : false;
        const smooth = options.smooth !== undefined ? options.smooth : 1;

        //

        const reflectorPlane = new Plane();
        const normal = new Vector3();
        const reflectorWorldPosition = new Vector3();
        const cameraWorldPosition = new Vector3();
        const rotationMatrix = new Matrix4();
        const lookAtPosition = new Vector3(0, 0, - 1);
        const clipPlane = new Vector4();

        const view = new Vector3();
        const target = new Vector3();
        const q = new Vector4();

        const textureMatrix = new Matrix4();
        const virtualCamera = this.camera;

        const renderTarget = new WebGLRenderTarget(textureWidth, textureHeight, { samples: multisample, type: HalfFloatType });
        this.renderTarget = renderTarget;


        const uniforms = {
            color: {
                value: color
            },
            tDiffuse: {
                value: renderTarget.texture
            },
            textureMatrix: {
                value: textureMatrix
            },
            uTexture: {
                value: map
            },
            uWrapS: {
                value: wrapS
            },
            uWrapT: {
                value: wrapT
            },
            gaussian: {
                value: gaussian
            },
            uSmooth: {
                value: smooth
            }
        };
        const shader = Reflector.shader;
        const material = new ShaderMaterial({
            name: 'unspecified',
            uniforms,
            fragmentShader: shader.fragmentShader,
            vertexShader: shader.vertexShader,
        });

        this.material = material;

        this.onBeforeRender = function (renderer, scene, camera: PerspectiveCamera) {

            reflectorWorldPosition.setFromMatrixPosition(scope.matrixWorld);
            cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);

            rotationMatrix.extractRotation(scope.matrixWorld);

            normal.set(0, 0, 1);
            normal.applyMatrix4(rotationMatrix);

            view.subVectors(reflectorWorldPosition, cameraWorldPosition);

            // Avoid rendering when reflector is facing away

            if (view.dot(normal) > 0) return;

            view.reflect(normal).negate();
            view.add(reflectorWorldPosition);

            rotationMatrix.extractRotation(camera.matrixWorld);

            lookAtPosition.set(0, 0, - 1);
            lookAtPosition.applyMatrix4(rotationMatrix);
            lookAtPosition.add(cameraWorldPosition);

            target.subVectors(reflectorWorldPosition, lookAtPosition);
            target.reflect(normal).negate();
            target.add(reflectorWorldPosition);

            virtualCamera.position.copy(view);
            virtualCamera.up.set(0, 1, 0);
            virtualCamera.up.applyMatrix4(rotationMatrix);
            virtualCamera.up.reflect(normal);
            virtualCamera.lookAt(target);

            virtualCamera.far = camera.far; // Used in WebGLBackground

            virtualCamera.updateMatrixWorld();
            virtualCamera.projectionMatrix.copy(camera.projectionMatrix);

            // Update the texture matrix
            textureMatrix.set(
                0.5, 0.0, 0.0, 0.5,
                0.0, 0.5, 0.0, 0.5,
                0.0, 0.0, 0.5, 0.5,
                0.0, 0.0, 0.0, 1.0
            );
            textureMatrix.multiply(virtualCamera.projectionMatrix);
            textureMatrix.multiply(virtualCamera.matrixWorldInverse);
            textureMatrix.multiply(scope.matrixWorld);

            // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
            // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
            reflectorPlane.setFromNormalAndCoplanarPoint(normal, reflectorWorldPosition);
            reflectorPlane.applyMatrix4(virtualCamera.matrixWorldInverse);

            clipPlane.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);

            const projectionMatrix = virtualCamera.projectionMatrix;

            q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
            q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
            q.z = - 1.0;
            q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];

            // Calculate the scaled plane vector
            clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));

            // Replacing the third row of the projection matrix
            projectionMatrix.elements[2] = clipPlane.x;
            projectionMatrix.elements[6] = clipPlane.y;
            projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias;
            projectionMatrix.elements[14] = clipPlane.w;

            // Render
            scope.visible = false;

            const currentRenderTarget = renderer.getRenderTarget();

            const currentXrEnabled = renderer.xr.enabled;
            const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;

            renderer.xr.enabled = false; // Avoid camera modification
            renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows

            renderer.setRenderTarget(renderTarget);

            renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897

            if (renderer.autoClear === false) renderer.clear();
            renderer.render(scene, virtualCamera);

            renderer.xr.enabled = currentXrEnabled;
            renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;

            renderer.setRenderTarget(currentRenderTarget);

            // Restore viewport

            const viewport = camera.viewport;

            if (viewport !== undefined) {

                renderer.state.viewport(viewport);

            }

            scope.visible = true;

        };

    }

    dispose = function () {

        this.renderTarget.dispose();
        this.material.dispose();

    };

    static shader = {

        name: 'ReflectorShader',

        vertexShader: /* glsl */`
            uniform mat4 textureMatrix;
            varying vec4 vUv;
            varying vec2 vUv2;
    
            #include <common>
            #include <logdepthbuf_pars_vertex>
    
            void main() {
    
                vUv = textureMatrix * vec4( position, 1.0 );
                vUv2 = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    
                #include <logdepthbuf_vertex>
    
            }`,

        fragmentShader: /* glsl */`
            uniform vec3 color;
            uniform sampler2D tDiffuse;
            uniform sampler2D uTexture;
            uniform float uWrapS;
            uniform float uWrapT;
            uniform float uSmooth;
            uniform bool gaussian;
            varying vec4 vUv;
            varying vec2 vUv2;

    
            #include <logdepthbuf_pars_fragment>
            #include <gaussian_blur_def>
    
            float blendOverlay( float base, float blend ) {
    
                return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
    
            }
    
            vec3 blendOverlay( vec3 base, vec3 blend ) {
    
                return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
    
            }
    
            void main() {
    
                #include <logdepthbuf_fragment>
    
                vec4 textureColor = texture2D(uTexture, fract(vec2(vUv2.x*uWrapS,vUv2.y*uWrapT)));

                vec4 reflectorColor = vec4(0.0);

                if(gaussian){
                    #include <gaussian_blur_exe>
                }else{
                    vec4 base = texture2DProj( tDiffuse, vUv );
                    reflectorColor = vec4( blendOverlay( base.rgb, color ), 1.0 );
                }

                gl_FragColor = mix(textureColor,reflectorColor,uSmooth);
    
                #include <tonemapping_fragment>
                #include <colorspace_fragment>
    
            }`
    };

}



